package karma.pool;

import karma.pool.log.RecorderFactory;
import karma.pool.pool.Pool;
import lombok.extern.slf4j.Slf4j;

import javax.sql.DataSource;
import java.io.Closeable;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.concurrent.atomic.AtomicBoolean;


@Slf4j
public class DataSourceImpl extends Configuration implements DataSource, Closeable {

   private final AtomicBoolean isShutdown = new AtomicBoolean();

   private final Pool fastPathPool;
   private volatile Pool pool;

   /**
    * Default constructor.  Setters are used to configure the pool.  Using
    * this constructor vs. {@link #DataSourceImpl(Configuration)} will
    * result in {@link #getConnection()} performance that is slightly lower
    * due to lazy initialization checks.
    * <p>
    * The first call to {@link #getConnection()} starts the pool.  Once the pool
    * is started, the configuration is "sealed" and no further configuration
    * changes are possible -- except via
    */
   public DataSourceImpl() {
      super();
      fastPathPool = null;
   }

   /**
    * Construct a DataSourceImpl with the specified configuration.  The
    * {@link Configuration} is copied and the pool is started by invoking this
    * constructor.
    * <p>
    * The {@link Configuration} can be modified without affecting the DataSourceImpl
    * and used to initialize another DataSourceImpl instance.
    *
    * @param configuration a Configuration instance
    */
   public DataSourceImpl(Configuration configuration) {
      configuration.validate();
      configuration.copyStateTo(this);

      log.info("{} - Starting...", configuration.getPoolName());
      pool = fastPathPool = new Pool(this);
      log.info("{} - Start completed.", configuration.getPoolName());

      this.seal();
   }

   public Pool getPool() {
      return pool;
   }

   // ***********************************************************************
   //                          DataSource methods
   // ***********************************************************************

   /**
    * {@inheritDoc}
    */
   @Override
   public Connection getConnection() throws SQLException {
      if (isClosed()) {
         throw new SQLException("DataSourceImpl " + this + " has been closed.");
      }

      if (fastPathPool != null) {
         return fastPathPool.getConnection();
      }

      // See http://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
      Pool pool = this.pool;
      if (pool == null) {
         synchronized (this) {
            pool = this.pool;
            if (pool == null) {
               validate();
               log.info("{} - Starting...", getPoolName());
               try {
                  this.pool = pool = new Pool(this);
                  this.seal();
               } catch (Pool.PoolInitializationException pie) {
                  if (pie.getCause() instanceof SQLException) {
                     throw (SQLException) pie.getCause();
                  } else {
                     throw pie;
                  }
               }
               log.info("{} - Start completed.", getPoolName());
            }
         }
      }

      return pool.getConnection();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public Connection getConnection(String username, String password) throws SQLException {
      throw new SQLFeatureNotSupportedException();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public PrintWriter getLogWriter() throws SQLException {
      Pool pool = this.pool;
      return (pool != null ? pool.getUnwrappedDataSource().getLogWriter() : null);
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void setLogWriter(PrintWriter out) throws SQLException {
      Pool pool = this.pool;
      if (pool != null) {
         pool.getUnwrappedDataSource().setLogWriter(out);
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public int getLoginTimeout() throws SQLException {
      Pool pool = this.pool;
      return (pool != null ? pool.getUnwrappedDataSource().getLoginTimeout() : 0);
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void setLoginTimeout(int seconds) throws SQLException {
      Pool pool = this.pool;
      if (pool != null) {
         pool.getUnwrappedDataSource().setLoginTimeout(seconds);
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
      throw new SQLFeatureNotSupportedException();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   @SuppressWarnings("unchecked")
   public <T> T unwrap(Class<T> iface) throws SQLException {
      if (iface.isInstance(this)) {
         return (T) this;
      }

      Pool pool = this.pool;
      if (pool != null) {
         final DataSource unwrappedDataSource = pool.getUnwrappedDataSource();
         if (iface.isInstance(unwrappedDataSource)) {
            return (T) unwrappedDataSource;
         }

         if (unwrappedDataSource != null) {
            return unwrappedDataSource.unwrap(iface);
         }
      }

      throw new SQLException("Wrapped DataSource is not an instance of " + iface);
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public boolean isWrapperFor(Class<?> iface) throws SQLException {
      if (iface.isInstance(this)) {
         return true;
      }

      Pool pool = this.pool;
      if (pool != null) {
         final DataSource unwrappedDataSource = pool.getUnwrappedDataSource();
         if (iface.isInstance(unwrappedDataSource)) {
            return true;
         }

         if (unwrappedDataSource != null) {
            return unwrappedDataSource.isWrapperFor(iface);
         }
      }

      return false;
   }

   // ***********************************************************************
   //                        HikariConfigMXBean methods
   // ***********************************************************************

   /**
    * {@inheritDoc}
    */
   @Override
   public void setMetricRegistry(Object metricRegistry) {
      boolean isAlreadySet = getMetricRegistry() != null;
      super.setMetricRegistry(metricRegistry);

      Pool pool = this.pool;
      if (pool != null) {
         if (isAlreadySet) {
            throw new IllegalStateException("MetricRegistry can only be set one time");
         } else {
            pool.setMetricRegistry(super.getMetricRegistry());
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void setRecorderFactory(RecorderFactory recorderFactory) {
      boolean isAlreadySet = getRecorderFactory() != null;
      super.setRecorderFactory(recorderFactory);

      Pool pool = this.pool;
      if (pool != null) {
         if (isAlreadySet) {
            throw new IllegalStateException("RecorderFactory can only be set one time");
         } else {
            pool.setMetricsTrackerFactory(super.getRecorderFactory());
         }
      }
   }


   // ***********************************************************************
   //                        HikariCP-specific methods
   // ***********************************************************************

   /**
    * Returns {@code true} if the pool as been started and is not suspended or shutdown.
    *
    * @return {@code true} if the pool as been started and is not suspended or shutdown.
    */
   public boolean isRunning() {
      return pool != null && pool.poolState == Pool.POOL_NORMAL;
   }


   /**
    * Evict a proxyConnection from the pool.  If the proxyConnection has already been closed (returned to the pool)
    * this may result in a "soft" eviction; the proxyConnection will be evicted sometime in the future if it is
    * currently in use.  If the proxyConnection has not been closed, the eviction is immediate.
    *
    * @param connection the proxyConnection to evict from the pool
    */
   public void evictConnection(Connection connection) {
      Pool pool;
      if (!isClosed() && (pool = this.pool) != null && connection.getClass().getName().startsWith("com.zaxxer.hikari")) {
         pool.evictConnection(connection);
      }
   }

   /**
    * Shutdown the DataSource and its associated pool.
    */
   @Override
   public void close() {
      if (isShutdown.getAndSet(true)) {
         return;
      }

      Pool pool = this.pool;
      if (pool != null) {
         try {
            log.info("{} - Shutdown initiated...", getPoolName());
            pool.shutdown();
            log.info("{} - Shutdown completed.", getPoolName());
         } catch (InterruptedException e) {
            log.warn("{} - Interrupted during closing", getPoolName(), e);
            Thread.currentThread().interrupt();
         }
      }
   }

   /**
    * Determine whether the DataSourceImpl has been closed.
    */
   public boolean isClosed() {
      return isShutdown.get();
   }


   @Override
   public String toString() {
      return "DataSourceImpl (" + pool + ")";
   }
}
